﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.IO;

namespace Util.IO
{
    /// <summary>
    /// Wraps around an underlying stream and restricts the user to read or
    /// write no more than the given number of bytes.
    /// </summary>
    public class LimitedLengthStream : Stream
    {
        private Stream baseStream;
        private bool leaveOpen;
        private long length;
        private long position;

        public LimitedLengthStream(Stream baseStream, long limitLength, bool leaveOpen)
        {
            if (baseStream == null)
                throw new ArgumentNullException("baseStream");
            if (limitLength < 0)
                throw new ArgumentException("viewLength must be greater than or equal to zero.");

            this.baseStream = baseStream;
            this.leaveOpen = leaveOpen;
            this.length = limitLength;
            this.position = 0;
        }

        private void CheckDisposed()
        {
            if (baseStream == null)
                throw new ObjectDisposedException("LimitedLengthStream");
        }

        public override bool CanRead
        {
            get { CheckDisposed(); return baseStream.CanRead; }
        }

        public override bool CanWrite
        {
            get { CheckDisposed(); return false; }
        }

        /// <summary>
        /// We do not allow seeking.
        /// </summary>
        public override bool CanSeek
        {
            get { CheckDisposed(); return baseStream.CanSeek; }
        }
 
        public override void Flush()
        {
            CheckDisposed();
            baseStream.Flush();
        }

        /// <summary>
        /// Gets the length of this stream. This value is the limitLength
        /// argument supplied in the constructor. This property is valid
        /// even after the stream is disposed.
        /// </summary>
        public override long Length         {            get { return length; }        }

        /// <summary>
        /// Gets the number of bytes successfully read or written so far.
        /// This property is valid even after the stream is disposed.
        /// </summary>
        public override long Position
        {
            get { return position; }
            set
            {
                CheckDisposed();
                throw new NotSupportedException("LimitedLengthStream does not support seeking.");
            }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            CheckDisposed();
            throw new NotSupportedException("LimitedLengthStream does not support seeking.");
#if false
            long pos;
            switch (origin)
            {
                case SeekOrigin.Begin:
                    pos = this.viewOffset;
                    break;
                case SeekOrigin.Current:
                    pos = this.viewOffset + this.Position;
                    break;
                case SeekOrigin.End:
                    pos = this.viewOffset + this.length;
                    break;
                default:
                    throw new ArgumentException("Invalid value in origin.");
            }

            pos += offset;
            if (pos < this.viewOffset)
                throw new ArgumentException("Cannot seek past the beginning of the stream.");

            return stream.Seek(pos, SeekOrigin.Begin);
#endif
        }

        /// <summary>
        /// Reads 'count' bytes into 'buffer', starting at 'offset'. This
        /// method limits the total number of bytes successfully read to
        /// the length limit supplied in the constructor. If the underlying
        /// stream throws an exception, the read is considered unsuccessful
        /// and the count is not cumulated.
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0 || offset > buffer.Length)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0 || offset + count > buffer.Length)
                throw new ArgumentOutOfRangeException("count");
            CheckDisposed();

            // Do not read over length limit.
            count = (int)Math.Min((long)count, this.length - this.position);
            int actual = baseStream.Read(buffer, offset, count);
            if (actual < 0 || actual > count)
            {
                throw new ArgumentException(
                    "The underlying stream returned an incorrect byte count.");
            }
            position += actual;
            return actual;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
#if false
            if (count < 0)
                throw new ArgumentException("count must be greater than or equal to zero.");
            if (this.Position + count >= this.length)
                throw new IOException("Cannot write beyond the end of the stream.");

            stream.Write(buffer, offset, count);
#endif
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException(
                "LimitedLengthStream does not allow setting the length.");
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (baseStream != null)
                {
                    if (!leaveOpen)
                        baseStream.Dispose();
                    baseStream = null;
                }
            }
        }
    }
}
